<?php
namespace App\Services\Wechat\sdk\components;

use App\Helpers\SnHelper;
use DOMDocument;
use DOMElement;
use DOMText;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpKernel\Exception\HttpException;

/**
 * 微信SDK操作基类
 *
 * @package callmez\wechat\sdk
 */
abstract class BaseWechat
{
    /**
     * Access Token更新后事件
     */
    const EVENT_AFTER_ACCESS_TOKEN_UPDATE = 'afterAccessTokenUpdate';
    /**
     * JS API更新后事件
     */
    const EVENT_AFTER_JS_API_TICKET_UPDATE = 'afterJsApiTicketUpdate';
    /**
     * 数据缓存前缀
     * @var string
     */
    public $cachePrefix = 'cache_wechat_sdk';
    /**
     * @var array
     */
    private $_accessToken;
    /**
     * @var array
     */
    private $_componentAccessToken;
    /**
     * @var array
     */
    private $_preAuthCode;

    /**
     * @var array
     */
    private $_jsApiTicket;
    /**
     * @var MessageCrypt
     */
    private $_messageCrypt;

    /**
     * 返回错误码
     * @var array
     */
    public $lastError;

    /**
     * 请求微信服务器获取AccessToken
     * 必须返回以下格式内容
     * [
     *     'access_token => 'xxx',
     *     'expirs_in' => 7200
     * ]
     * @return array|bool
     */
    abstract protected function requestAccessToken();

    /**
     * 请求微信服务器获取AccessToken
     * 必须返回以下格式内容
     * [
     *     'component_access_token => 'xxx',
     *     'expirs_in' => 7200
     * ]
     * @return array|bool
     */
    abstract protected function requestComponentAccessToken();

    /**
     * 请求微信服务器获取微信开放平台预授权码
     * 必须返回以下格式内容
     * [
     *     'pre_auth_code => 'xxx',
     *     'expirs_in' => 7200
     * ]
     * @return array|bool
     */
    abstract protected function requestComponentPreAuthCode();

    /**
     * 获取AccessToken
     * 超时后会自动重新获取AccessToken并触发self::EVENT_AFTER_ACCESS_TOKEN_UPDATE事件
     * @param bool $force 是否强制获取
     * @return mixed
     * @throws HttpException
     */
    public function getAccessToken($force = false)
    {
        $time = time(); // 为了更精确控制.取当前时间计算
        if (empty($this->_accessToken) || $this->_accessToken['expire'] < $time || $force) {
            $result = empty($this->_accessToken) && !$force ? $this->getCache('access_token') : false;
            if (!$result) {
                if (!($result = $this->requestAccessToken())) {
                    //throw new HttpException(500, 'Fail to get access_token from wechat server.');
                    return false;
                }
                $result['expire'] = $time + $result['expires_in'];

                $this->setCache('access_token', $result, $result['expires_in']);
            }
            $this->setAccessToken($result);
        }
        return $this->_accessToken['access_token'];
    }

    /**
     * 设置AccessToken
     * @param array $accessToken
     * @throws Exception
     */
    public function setAccessToken(array $accessToken)
    {
        $this->_accessToken = $accessToken;
    }

    /**
     * 获取ComponentAccessToken
     * 超时后会自动重新获取ComponentAccessToken并触发self::EVENT_AFTER_ACCESS_TOKEN_UPDATE事件
     * @param bool $force 是否强制获取
     * @return mixed
     * @throws HttpException
     */
    public function getComponentAccessToken($force = false)
    {
        $time = time(); // 为了更精确控制.取当前时间计算
        if (empty($this->_componentAccessToken) || $this->_componentAccessToken['expire'] < $time || $force) {
            $result = empty($this->_componentAccessToken) && !$force ? $this->getCache('component_access_token') : false;
            if (!$result) {
                if (!($result = $this->requestComponentAccessToken())) {
                    return false;
                }
                $result['expire'] = $time + $result['expires_in'];
                SnHelper::logFun('获取ComponentAccessToken返回数据','过期时间：'.date('Y-m-d H:i:s',$result['expire']).'||'.'有效时间：'.$result['expires_in'].'||'.'ticket：'.$result['component_access_token']);
                $this->setCache('component_access_token', $result, $result['expires_in']);
            }
            $this->setComponentAccessToken($result);
        }
        return $this->_componentAccessToken['component_access_token'];
    }

    /**
     * 设置ComponentAccessToken
     * @param array $componentAccessToken
     * @throws Exception
     */
    public function setComponentAccessToken(array $componentAccessToken)
    {
        $this->_componentAccessToken = $componentAccessToken;
    }

    /**
     * 获取PreAuthCode
     * @param bool $force 是否强制获取
     * @return mixed
     * @throws HttpException
     */
    public function getComponentPreAuthCode($force = false)
    {
        $time = time(); // 为了更精确控制.取当前时间计算
        if (empty($this->_preAuthCode) || $this->_preAuthCode['expire'] < $time || $force) {
            $result = empty($this->_preAuthCode) && !$force ? $this->getCache('pre_auth_code') : false;
            if (!$result) {
                if (!($result = $this->requestComponentPreAuthCode())) {
                    return false;
                }
                $result['expire'] = $time + $result['expires_in'];
                SnHelper::logFun('获取PreAuthCode返回数据','过期时间：'.date('Y-m-d H:i:s',$result['expire']).'||'.'有效时间：'.$result['expires_in'].'||'.'ticket：'.$result['pre_auth_code']);
                $this->setCache('pre_auth_code', $result, $result['expires_in']);
            }
            $this->setComponentPreAuthCode($result);
        }
        return $this->_preAuthCode['pre_auth_code'];
    }

    /**
     * 设置PreAuthCode
     * @param array $preAuthCode
     * @throws Exception
     */
    public function setComponentPreAuthCode(array $preAuthCode)
    {
        $this->_preAuthCode = $preAuthCode;
    }


    /**
     * 请求微信服务器获取JsApiTicket
     * 必须返回以下格式内容
     * [
     *     'ticket => 'xxx',
     *     'expirs_in' => 7200
     * ]
     * @return array|bool
     */
    abstract public function requestJsApiTicket();

    /**
     * 生成js 必要的config
     */
    abstract public function jsApiConfig(array $config = []);

    /**
     * 获取js api ticket
     * 超时后会自动重新获取JsApiTicket并触发self::EVENT_AFTER_JS_API_TICKET_UPDATE事件
     * @param bool $force 是否强制获取
     * @return mixed
     * @throws HttpException
     */
    public function getJsApiTicket($force = false)
    {
        $time = time(); // 为了更精确控制.取当前时间计算
        if (empty($this->_jsApiTicket) || $this->_jsApiTicket['expire'] < $time || $force) {
            $result = empty($this->_jsApiTicket) && !$force ? $this->getCache('js_api_ticket') : false;
            if (!$result) {
                if (!($result = $this->requestJsApiTicket())) {
                    //throw new HttpException(500, 'Fail to get jsapi_ticket from wechat server.');
                }
                $result['expire'] = $time + $result['expires_in'];
                $this->setCache('js_api_ticket', $result, $result['expires_in']);
            }
            $this->setJsApiTicket($result);
        }
        return $this->_jsApiTicket['ticket'];
    }

    /**
     * 设置JsApiTicket
     * @param array $jsApiTicket
     */
    public function setJsApiTicket(array $jsApiTicket)
    {
        $this->_jsApiTicket = $jsApiTicket;
    }

    /**
     * 创建消息加密类
     * @return mixed
     */
    abstract protected function createMessageCrypt();

    /**
     * 设置消息加密处理类
     * @return MessageCrypt
     */
    public function getMessageCrypt()
    {
        if ($this->_messageCrypt === null) {
            $this->setMessageCrypt($this->createMessageCrypt());
        }
        return $this->_messageCrypt;
    }

    /**
     * 设置消息加密处理类
     * @param MessageCrypt $messageCrypt
     */
    public function setMessageCrypt(MessageCrypt $messageCrypt)
    {
        $this->_messageCrypt = $messageCrypt;
    }

    /**
     * 加密XML数据
     * @param string $xml 加密的XML
     * @param string $timestamp 加密时间戳
     * @param string $nonce 加密随机串
     * @return string|bool
     */
    public function encryptXml($xml, $timestamp , $nonce)
    {
        $errorCode = $this->getMessageCrypt()->encryptMsg($xml, $timestamp, $nonce, $xml);
        if ($errorCode) {
            $this->lastError = [
                'errcode' => $errorCode,
                'errmsg' => 'XML数据加密失败!'
            ];
            return false;
        }
        return $xml;
    }

    /**
     * 解密XML数据
     * @param string $xml 解密的XML
     * @param string $messageSignature 加密签名
     * @param string $timestamp 加密时间戳
     * @param string $nonce 加密随机串
     * @return string|bool
     */
    public function decryptXml($xml, $messageSignature, $timestamp , $nonce)
    {
        $errorCode = $this->getMessageCrypt()->decryptMsg($messageSignature, $timestamp, $nonce, $xml, $xml);
        if ($errorCode) {
            $this->lastError = [
                'errcode' => $errorCode,
                'errmsg' => 'XML数据解密失败!'
            ];
            return false;
        }
        return $xml;
    }

    /**
     * 创建微信格式的XML
     * @param array $data
     * @param null $charset
     * @return string
     */
    public function xml(array $data, $charset = null)
    {
        $dom = new DOMDocument('1.0', $charset === null ? 'utf-8' : $charset);
        $root = new DOMElement('xml');
        $dom->appendChild($root);
        $this->buildXml($root, $data);
        $xml = $dom->saveXML();
        return trim(substr($xml, strpos($xml, '?>') + 2));
    }

    /**
     *
     */
    protected function buildXml($element, $data)
    {
        if (is_object($data)) {
            $child = new DOMElement($this->basename(get_class($data)));
            $element->appendChild($child);
            $array = [];
            foreach ($data as $name => $value) {
                $array[$name] = $value;
            }
            $this->buildXml($child, $array);
        } elseif (is_array($data)) {
            foreach ($data as $name => $value) {
                if (is_int($name) && is_object($value)) {
                    $this->buildXml($element, $value);
                } elseif (is_array($value) || is_object($value)) {
                    $child = new DOMElement(is_int($name) ? $this->itemTag : $name);
                    $element->appendChild($child);
                    $this->buildXml($child, $value);
                } else {
                    $child = new DOMElement(is_int($name) ? $this->itemTag : $name);
                    $element->appendChild($child);
                    $child->appendChild(new DOMText((string) $value));
                }
            }
        } else {
            $element->appendChild(new DOMText((string) $data));
        }
    }


    public function basename($path, $suffix = '')
    {
        if (($len = mb_strlen($suffix)) > 0 && mb_substr($path, -$len) === $suffix) {
            $path = mb_substr($path, 0, -$len);
        }
        $path = rtrim(str_replace('\\', '/', $path), '/\\');
        if (($pos = mb_strrpos($path, '/')) !== false) {
            return mb_substr($path, $pos + 1);
        }

        return $path;
    }

    /**
     * 微信数据缓存基本键值
     * @param $name
     * @return string
     */
    abstract protected function getCacheKey($name);

    /**
     * 缓存微信数据
     * @param $name
     * @param $value
     * @param null $duration
     * @return bool
     */
    protected function setCache($name, $value, $duration = 0)
    {
        return Cache::add($this->getCacheKey($name), $value, $duration);
    }

    /**
     * 获取微信缓存数据
     * @param $name
     * @return mixed
     */
    public function getCache($name)
    {
        $cache_data = Cache::get($this->getCacheKey($name));
        if(!empty($cache_data['expire']) && $cache_data['expire'] < time()){
            Cache::forget($this->getCacheKey($name));
        }
        return $cache_data;
    }

    /**
     * Api url 组装
     * @param $url
     * @param array $options
     * @return string
     */
    protected function httpBuildQuery($url, array $options)
    {
        if (!empty($options)) {
            $url .= (stripos($url, '?') === null ? '&' : '?') . http_build_query($options);
        }
        return $url;
    }

    /**
     * Http Get 请求
     * @param $url
     * @param array $options
     * @return mixed
     */
    public function httpGet($url, array $options = [])
    {
        return $this->parseHttpRequest(function($url) {
            return $this->http($url);
        }, $this->httpBuildQuery($url, $options));
    }

    /**
     * Http Post 请求
     * @param $url
     * @param array $postOptions
     * @param array $options
     * @return mixed
     */
    public function httpPost($url, array $postOptions, array $options = [])
    {
        return $this->parseHttpRequest(function($url, $postOptions) {
            return $this->http($url, [
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => $postOptions
            ]);
        }, $this->httpBuildQuery($url, $options), $postOptions);
    }

    /**
     * Http Raw数据 Post 请求
     * @param $url
     * @param $postOptions
     * @param array $options
     * @return mixed
     */
    public function httpRaw($url, $postOptions, array $options = [])
    {
        return $this->parseHttpRequest(function($url, $postOptions) {
            return $this->http($url, [
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => is_array($postOptions) ? json_encode($postOptions, JSON_UNESCAPED_UNICODE) : $postOptions
            ]);
        }, $this->httpBuildQuery($url, $options), $postOptions);
    }

    /**
     * 解析微信请求响应内容
     * @param callable $callable Http请求主体函数
     * @param string $url Api地址
     * @param array|string|null $postOptions Api地址一般所需要的post参数
     * @return array|bool
     */
    abstract public function parseHttpRequest(callable $callable, $url, $postOptions = null);

    /**
     * Http基础库 使用该库请求微信服务器
     * @param $url
     * @param array $options
     * @return bool|mixed
     */
    protected function http($url, $options = [])
    {
        $options = [
            CURLOPT_URL => $url,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_CONNECTTIMEOUT => 30,
            CURLOPT_RETURNTRANSFER => true,
        ] + (stripos($url, "https://") !== false ? [
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1 // 微信官方屏蔽了ssl2和ssl3, 启用更高级的ssl
        ] : []) + $options;

        $curl = curl_init();
        curl_setopt_array($curl, $options);
        $content = curl_exec($curl);
        $status = curl_getinfo($curl);
        curl_close($curl);
        if (isset($status['http_code']) && $status['http_code'] == 200) {
            return json_decode($content, true) ?: false; // 正常加载应该是只返回json字符串
        }
        return false;
    }

    /**
     * 上传文件请使用该类来解决curl版本兼容问题
     * @param $filePath
     * @return \CURLFile|string
     */
    public function uploadFile($filePath)
    {
        // php 5.5将抛弃@写法,引用CURLFile类来实现 @see http://segmentfault.com/a/1190000000725185
        return class_exists('\CURLFile') ? new \CURLFile($filePath,'','') : '@' . $filePath;
    }

    public $_hostInfo;

    public function getHostInfo()
    {
        if ($this->_hostInfo === null) {
            $secure = $this->getIsSecureConnection();
            $http = $secure ? 'https' : 'http';
            if (isset($_SERVER['HTTP_HOST'])) {
                $host = explode(':',$_SERVER['HTTP_HOST']);
                if(!empty($host[0]) && ($host[0]== 'localhost' || $host[0]== '127.0.0.1')){
                    $_host = explode('://',$_SERVER['HTTP_ORIGIN']);
                    $this->_hostInfo = $http . '://' . $_host[1];
                }else{
                    $this->_hostInfo = $http . '://' . $_SERVER['HTTP_HOST'];
                }
            } elseif (isset($_SERVER['SERVER_NAME'])) {
                $this->_hostInfo = $http . '://' . $_SERVER['SERVER_NAME'];
                $port = $secure ? $this->getSecurePort() : $this->getPort();
                if (($port !== 80 && !$secure) || ($port !== 443 && $secure)) {
                    $this->_hostInfo .= ':' . $port;
                }
            }
        }

        return $this->_hostInfo;
    }

    private $_securePort;

    /**
     * Returns the port to use for secure requests.
     * Defaults to 443, or the port specified by the server if the current
     * request is secure.
     * @return integer port number for secure requests.
     * @see setSecurePort()
     */
    private function getSecurePort()
    {
        if ($this->_securePort === null) {
            $this->_securePort = $this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : 443;
        }

        return $this->_securePort;
    }

    private $_port;

    /**
     * Returns the port to use for insecure requests.
     * Defaults to 80, or the port specified by the server if the current
     * request is insecure.
     * @return integer port number for insecure requests.
     * @see setPort()
     */
    private function getPort()
    {
        if ($this->_port === null) {
            $this->_port = !$this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : 80;
        }

        return $this->_port;
    }

    /**
     * Return if the request is sent via secure channel (https).
     * @return boolean if the request is sent via secure channel (https)
     */
    private function getIsSecureConnection()
    {
        return isset($_SERVER['HTTPS']) && (strcasecmp($_SERVER['HTTPS'], 'on') === 0 || $_SERVER['HTTPS'] == 1)
            || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0;
    }


    public function resolveRequestUri()
    {
        if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { // IIS
            $requestUri = $_SERVER['HTTP_X_REWRITE_URL'];
        } elseif (isset($_SERVER['REQUEST_URI'])) {
            $requestUri = $_SERVER['REQUEST_URI'];
            if ($requestUri !== '' && $requestUri[0] !== '/') {
                $requestUri = preg_replace('/^(http|https):\/\/[^\/]+/i', '', $requestUri);
            }
        } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0 CGI
            $requestUri = $_SERVER['ORIG_PATH_INFO'];
            if (!empty($_SERVER['QUERY_STRING'])) {
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }
        } else {
            throw new \Exception('Unable to determine the request URI.');
        }

        return $requestUri;
    }
}
